package moltencore.game;

import moltencore.game.bubble.AbstractBubble;
import moltencore.game.collision.ICollidable;
import org.apache.log4j.Logger;
import spark.IUpdateable;
import spark.math.Vector2;
import spark.utility.OutBoolean;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;

public abstract class AbstractField<BubbleType extends AbstractBubble, PlayerType extends AbstractPlayer> implements IUpdateable {
	private static Logger logger = Logger.getLogger(AbstractField.class);

	protected static final Vector2 DEFAULT_GRAVITY = new Vector2(0f, -10f);

	protected long fieldId;
	protected ArrayList<PlayerType> owners = new ArrayList<PlayerType>();

	protected final String fieldFile;
	protected float score = 0;
	protected Vector2 gravity;
	protected Vector2 wind;
	protected ArrayList<ICollidable> collidables;
	protected ArrayList<ICollidable> criticalAreas;
	protected HashMap<Integer, CannonData> cannonData = new HashMap<Integer, CannonData>();

	public AbstractField(long fieldId, String fieldFile,
	                     boolean windEnabled, boolean gravityEnabled,
	                     ArrayList<ICollidable> collidables, ArrayList<ICollidable> criticalAreas, Collection<CannonData> cannonData) {
		this.fieldFile = fieldFile;
		this.fieldId = fieldId;
		this.gravity = (gravityEnabled) ? new Vector2(DEFAULT_GRAVITY) : null;
		this.wind = (windEnabled) ? new Vector2() : null;
		this.collidables = collidables;
		this.criticalAreas = criticalAreas;
		for(CannonData data : cannonData) {
			this.cannonData.put(data.getIndex(), data);
		}
	}

	public long getFieldId() {
		return fieldId;
	}

	protected void applyGravity(BubbleType bubble, Vector2 gravity, float elapsedTimeSeconds) {
		if(gravity != null) {
			bubble.getVelocity().add(Vector2.multiply(gravity, elapsedTimeSeconds));
		}
	}

	protected void applyMovement(BubbleType bubble, Vector2 movement) {
		bubble.getPosition().add(movement);
	}

	/**
	 * Handles collision of a bubble against the fields collidables
	 * @param bubble                Bubble to check collisions against
	 * @param movement              Movement this bubble will travel
	 * @param collidedWithSticky    Value set to true if the bubble collides with a sticky collidable (ignored if null)
	 * @return true if a collision occured, false otherwise
	 */
	protected boolean handleCollidablesCollision(BubbleType bubble, Vector2 movement, OutBoolean collidedWithSticky) {
		//check collision against field's collidables
		for(ICollidable collidable : collidables) {
			boolean collisionOccurred = handleMovingBubbleCollisionLogic(collidable, bubble, movement, collidedWithSticky);
			if(collisionOccurred) {
				return true;
			}
		}
		return false;
	}

	/**
	 * If the bubble would collide against the collidable when the movement vector is applied to it this method
	 * handles the collision response.
	 * @param collidable            Collidable the bubble might collide against
	 * @param bubble                The bubble in question
	 * @param movement              Movement that the method assumes the bubble will travel
	 * @param collidedWithSticky    Value set to true if the bubble collides with a sticky collidable (ignored if null)
	 * @return                      true if a collision occured, false otherwise
	 */
	protected boolean handleMovingBubbleCollisionLogic(ICollidable collidable, BubbleType bubble, Vector2 movement, OutBoolean collidedWithSticky) {
		Vector2 collisionPoint = collidable.collisionPoint(bubble, movement);
		if(collisionPoint == null) {
			return false;
		}
		// if the bubble collides against something set its position to the collision point and don't
		// move it this frame, part of the movement might get lost but this is neglectable
		bubble.getPosition().copy(collisionPoint);
		movement.x = movement.y = 0;

		if(collidedWithSticky != null) {
			collidedWithSticky.value = collidable.isSticky();
		}

		collidable.collisionResponse(bubble);
		return true;
	}

	protected Vector2 calculateMovement(BubbleType bubble, float elapsedTimeSeconds) {
		return Vector2.multiply(bubble.getVelocity(), elapsedTimeSeconds);
	}

	public Collection<CannonData> getCannonData() {
		return cannonData.values();
	}

	public CannonData getCannonData(int cannonPositionIndex) {
		return cannonData.get(cannonPositionIndex);
	}

	public String getFieldFile() {
		return fieldFile;
	}

	public void addOwner(PlayerType owner) {
		owners.add(owner);
	}

	public Collection<PlayerType> getOwners() {
		return owners;
	}

	public float getScore() {
		return score;
	}

	public void setScore(float score) {
		this.score = score;
	}

	public abstract void pop(BubbleType bubble);

	@Override
	public boolean equals(Object o) {
		if(this == o) return true;
		if(o == null || getClass() != o.getClass()) return false;

		Field field = (Field) o;

		if(fieldId != field.fieldId) return false;

		return true;
	}

	@Override
	public int hashCode() {
		return (int) (fieldId ^ (fieldId >>> 32));
	}
}
